/*
 ------------------------------------------------------------------------------
	winmad++ -	a test program for using SDL and mad library on Windows
				with object oriented UI style
				with low level mad (seek)

	Copyright (C) 2011 Pierre Veber
	
	This program is free software; you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation; either version 3 of the License, or
	(at your option) any later version.
	
	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.
	
	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software Foundation,
	Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
------------------------------------------------------------------------------
*/

#include "precomp.h"

#define AUDIO_BLOCK_SIZE	8192
#define AUDIO_BUFFER_SLOTS	3

static WAVEHDR* getFreeBlock(WAVEHDR* pBlocks, int* pFreeNum)
{
	WAVEHDR* pRet = NULL;
	*pFreeNum = 0;
	for (int i=0; i < AUDIO_BUFFER_SLOTS; i++)
	{
		if ( (pBlocks[i].dwBufferLength < AUDIO_BLOCK_SIZE) || (pBlocks[i].dwFlags & WHDR_DONE) )
		{
			pRet = &pBlocks[i];
			(*pFreeNum)++;
		}
	}
	return pRet;
}

static void initBlocks(WAVEHDR* pBlocks, LPSTR pWaveData)
{
	for (int i=0; i < AUDIO_BUFFER_SLOTS; i++)
	{
		memset(&pBlocks[i], 0, sizeof(WAVEHDR));
		// assign data block address to corresponding header
		pBlocks[i].lpData = pWaveData + (i * AUDIO_BLOCK_SIZE);
		pBlocks[i].dwUser = (DWORD)i;
	}
}

void CALLBACK waveOutProc(
					HWAVEOUT hWaveOut, 
					UINT uMsg, 
					DWORD dwInstance, 
					DWORD dwParam1,
					DWORD dwParam2 
					)
{
	if (uMsg == WOM_DONE)
	{
		WAVEHDR* pBlock = (WAVEHDR*)dwParam1;
		//TRACE("waveOutProc: block %d has finished reading.\n", pBlock->dwUser);
		// reset block buffer length
		pBlock->dwBufferLength = 0;
		// trigger free block event
		SetEvent((HANDLE)dwInstance);
	}
}

int waveWriteIn(
			audio_data_t* pAudio,
			const void* pBuffer,
			unsigned int frames
			)
{
	WAVEHDR* pBlock;
	DWORD dwCause;
	int freeblocks;
	size_t bytesrem;
	size_t byteslen;

	// wait for free block
	do
	{
		pBlock = getFreeBlock(pAudio->pHdr, &freeblocks);
		if (pBlock == NULL)	
		{
			//TRACE("No free blocks, wait for free block event.\n");
			dwCause = WaitForSingleObject(pAudio->hFreeBlock, INFINITE);
			if (dwCause != WAIT_OBJECT_0)
			{
				// event error
				fprintf(stderr, "waveWriteIn - event error!\n");
				return -1;
			}
			//TRACE("Event free block received.\n");
		}
	} while (pBlock == NULL);
	
	// check that the block is large enough to contain data:
	byteslen = (size_t)(4 * frames) /* 16 bit stereo */;
	if ( (pBlock->dwBufferLength + byteslen) < AUDIO_BLOCK_SIZE )
	{
		// if large enough
		// fill the free block with data provided and exit
		memcpy(&(pBlock->lpData)[pBlock->dwBufferLength], (char*)pBuffer, byteslen);
		pBlock->dwBufferLength += byteslen;
	}
	else
	{
		// if not large enough
		// fill the free block with as much data as possible
		bytesrem = (size_t)(AUDIO_BLOCK_SIZE - pBlock->dwBufferLength);
		memcpy(&(pBlock->lpData)[pBlock->dwBufferLength], (char*)pBuffer, bytesrem);
		pBlock->dwBufferLength = AUDIO_BLOCK_SIZE;
		
		// send the block to the device
		if (pBlock->dwFlags & WHDR_DONE)
		{
			waveOutUnprepareHeader(pAudio->hWaveOut, pBlock, sizeof(WAVEHDR));
		}
		waveOutPrepareHeader(pAudio->hWaveOut, pBlock, sizeof(WAVEHDR));
		waveOutWrite(pAudio->hWaveOut, pBlock, sizeof(WAVEHDR));

		// if any data remains, call waveWriteIn with remaining data
		if (byteslen > bytesrem)
		{
			waveWriteIn(pAudio, &((char*)pBuffer)[bytesrem], (unsigned int)(byteslen-bytesrem)/4);
		}
		return (int)(bytesrem / 4);
	}
	return (int)(byteslen / 4);
}

int waveInit(
			audio_data_t* pAudio
			)
{
	WAVEFORMATEX wfx;

	// create freeblock event
	pAudio->hFreeBlock = CreateEvent(NULL, FALSE, FALSE, _T("FreeBlockEvent"));
	if (pAudio->hFreeBlock == NULL)
	{
		fprintf(stderr, "waveInit -  unable to create event\n");
		return 1;
	}

	/* open wave device */
	wfx.nSamplesPerSec = 44100;
	wfx.wBitsPerSample = 16;
	wfx.nChannels= 2;
	wfx.cbSize = 0;
	wfx.wFormatTag = WAVE_FORMAT_PCM;
	wfx.nBlockAlign = (wfx.wBitsPerSample * wfx.nChannels) >> 3;
	wfx.nAvgBytesPerSec = wfx.nBlockAlign * wfx.nSamplesPerSec;
	pAudio->hWaveOut = NULL;
	if ( waveOutOpen(
			&(pAudio->hWaveOut), 
			WAVE_MAPPER, 
			&wfx,
			(DWORD_PTR)waveOutProc, 
			(DWORD_PTR)pAudio->hFreeBlock, 
			CALLBACK_FUNCTION ) != MMSYSERR_NOERROR )
	{
		fprintf(stderr, "waveInit - Unable to open wave mapper device!\n");
		return 2;
	}

	// allocate memory for wave headers and wave data
	pAudio->pWaveData = (LPSTR)malloc(AUDIO_BLOCK_SIZE * AUDIO_BUFFER_SLOTS);
	pAudio->pHdr = (WAVEHDR*)malloc(sizeof(WAVEHDR) * AUDIO_BUFFER_SLOTS);
	if ( (pAudio->pWaveData == NULL) || (pAudio->pHdr == NULL) )
	{
		fprintf(stderr, "waveInit - unable to allocate memory for audio buffers\n");
		return 3;
	}
	
	// initialize buffer blocks
	initBlocks(pAudio->pHdr, pAudio->pWaveData);

	return 0;
}

void waveReset(
			audio_data_t* pAudio
			   )
{
	// flush remaining buffers
	for (int i=0; i < AUDIO_BUFFER_SLOTS; i++)
	{
		if ( (pAudio->pHdr[i].dwBufferLength > 0) && !(pAudio->pHdr[i].dwFlags & WHDR_DONE) )
		{
			waveOutPrepareHeader(pAudio->hWaveOut, &(pAudio->pHdr)[i], sizeof(WAVEHDR));
			waveOutWrite(pAudio->hWaveOut, &(pAudio->pHdr[i]), sizeof(WAVEHDR));

			// wait for block to be read - only one second to prevent deadlocks.
			WaitForSingleObject(pAudio->hFreeBlock, 1000);
		}
	}
	waveOutReset(pAudio->hWaveOut);
}

void waveClose(
			audio_data_t* pAudio
			)
{
	if (pAudio->hWaveOut != NULL)
	{
		waveReset(pAudio);
		waveOutClose(pAudio->hWaveOut);
	}

	if (pAudio->pWaveData != NULL)
	{
		free(pAudio->pWaveData);
	}
	if (pAudio->pHdr != NULL)
	{
		free(pAudio->pHdr);
	}
}
